Scapy è una potentissima libreria Python che rappresenta molto più di un semplice strumento di analisi di rete. È, in sostanza, un framework completo per la manipolazione di pacchetti di rete che opera a tutti i livelli dello stack protocollare, dal livello fisico fino al livello applicativo. Per comprendere appieno il valore di Scapy, dobbiamo prima capire cosa significa “manipolare pacchetti di rete” e perché questo è così importante nel contesto dell’informatica moderna.
Quando parliamo di comunicazione di rete, ci riferiamo essenzialmente allo scambio di pacchetti - piccole unità di dati che viaggiano attraverso cavi, onde radio o fibra ottica, trasportando informazioni da un dispositivo all’altro. Ogni pacchetto è come una lettera: ha un mittente, un destinatario, un contenuto e una struttura ben definita che permette ai dispositivi di rete di instradarlo correttamente verso la sua destinazione finale.
La maggior parte degli strumenti di rete tradizionali (come Wireshark, tcpdump o anche ping) permettono di osservare questi pacchetti mentre transitano sulla rete, un po’ come se fossimo degli spettatori che guardano il traffico stradale. Scapy, invece, ci mette direttamente al volante: non solo possiamo osservare i pacchetti, ma possiamo anche costruirli da zero, modificarli, inviarli e analizzare le risposte con un controllo totale e granulare su ogni singolo bit che compone il pacchetto.
Immaginate di dover costruire una casa. Potreste usare mattoni prefabbricati di dimensioni e forme standard (approccio tradizionale delle librerie di rete), oppure potreste avere accesso a un laboratorio dove potete creare mattoni personalizzati di qualsiasi forma, dimensione e materiale desideriate (approccio di Scapy). Quest’ultima opzione vi dà una flessibilità enormemente maggiore, ma richiede anche una comprensione più profonda di come funzionano realmente le cose.
La filosofia di design di Scapy si basa su tre pilastri fondamentali:
Semplicità d’uso: Nonostante la sua potenza, Scapy rende incredibilmente semplici operazioni che normalmente richiederebbero centinaia di righe di codice. Per esempio, creare e inviare un pacchetto ARP richiede appena 2-3 righe di codice Python.
Flessibilità totale: Non esistono limitazioni su cosa potete fare. Volete creare un pacchetto con campi non standard? Nessun problema. Volete modificare il TTL di un pacchetto IP? Facilissimo. Volete simulare un particolare comportamento di rete per testare come reagisce il vostro firewall? Scapy è lo strumento giusto.
Interattività: Scapy può essere usato sia in modalità interattiva (molto utile per sperimentare e imparare) sia all’interno di script Python complessi (perfetto per automatizzare task ripetitivi o creare strumenti personalizzati).
Nel contesto didattico, Scapy rappresenta uno strumento pedagogico di valore inestimabile. Quando si studiano i protocolli di rete sui libri o nelle slide delle lezioni, si apprendono concetti teorici: “ARP risolve indirizzi IP in indirizzi MAC”, “Ethernet incapsula i pacchetti IP”, “TCP usa un three-way handshake per stabilire connessioni”. Ma spesso questi concetti rimangono astratti, difficili da visualizzare concretamente.
Con Scapy, invece, potete toccare con mano questi concetti. Potete costruire un pacchetto ARP, vederne esattamente la struttura byte per byte, inviarlo sulla rete e vedere cosa succede. Potete modificare un campo, reinviarlo e osservare come cambia il comportamento dei dispositivi di rete. Questo approccio “hands-on” trasforma l’apprendimento da passivo ad attivo, permettendo di sviluppare una comprensione profonda e intuitiva di come funzionano realmente le reti.
Le applicazioni pratiche di Scapy sono vastissime e spaziano attraverso diversi domini dell’informatica:
1. Network Analysis e Troubleshooting
Quando una rete non funziona come dovrebbe, Scapy permette di investigare a fondo. Potete inviare pacchetti specifici per testare il comportamento di router, switch o firewall, analizzando le risposte per identificare esattamente dove si trova il problema. È come avere un microscopio elettronico per le reti.
2. Security Testing e Penetration Testing
Nel campo della cybersecurity, Scapy è uno strumento standard. Permette di testare la robustezza di sistemi di rete simulando vari tipi di attacchi (sempre in modo etico e autorizzato!). Potete verificare se un firewall filtra correttamente pacchetti malformati, testare la resistenza a ARP spoofing, o verificare la corretta implementazione di protocolli di sicurezza.
3. Sviluppo e Testing di Protocolli
Se state sviluppando un nuovo protocollo di rete o implementando uno esistente, Scapy permette di creare test cases complessi. Potete simulare vari scenari di rete, generare traffico anomalo per testare la robustezza, verificare la conformità a specifiche RFC.
4. Network Discovery e Mapping
Scapy eccelle nella discovery di reti: potete scoprire quali dispositivi sono attivi, quali servizi stanno eseguendo, come sono configurati. Tutto questo con un controllo molto più fine rispetto a strumenti standard come nmap.
5. Ricerca e Sperimentazione
Nel mondo accademico e della ricerca, Scapy è ampiamente utilizzato per sperimentare con nuove idee, testare ipotesi su comportamenti di rete, raccogliere dati per analisi statistiche sul traffico di rete.
A livello tecnico, Scapy lavora direttamente con le socket raw (socket grezze) del sistema operativo. Le socket raw permettono a un programma di accedere ai protocolli di rete a un livello molto basso, bypassando gran parte dello stack di rete del sistema operativo. Questo significa che quando create un pacchetto con Scapy, state letteralmente specificando ogni singolo byte che verrà messo “sul filo” (o trasmesso via wireless).
Quando usate strumenti di rete normali (come un browser web o un client email), il sistema operativo gestisce automaticamente la creazione dei pacchetti: voi fornite i dati di alto livello ("vai su www.example.com") e il sistema operativo si occupa di tutto il resto (risoluzione DNS, creazione pacchetti TCP/IP, frammentazione, ritrasmissione in caso di errori, ecc.). Con Scapy, invece, questo controllo automatico viene meno: siete voi a decidere esattamente cosa fare.
Questo approccio ha dei pro e dei contro:
Vantaggi:
Svantaggi:
Una delle caratteristiche distintive di Scapy è la sua eccellente modalità interattiva. Potete avviare Scapy dalla shell Python e iniziare immediatamente a sperimentare:
$ sudo scapy
Welcome to Scapy
>>> packet = IP(dst="192.168.1.1")/ICMP()
>>> packet.show()
>>> send(packet)
Questa modalità è fantastica per:
Tuttavia, per task più complessi o ripetitivi, è meglio scrivere script Python completi che importano Scapy come libreria. Questo approccio permette di:
In questa guida utilizzeremo principalmente l’approccio degli script Python, ma tenete presente che tutto può essere fatto anche interattivamente.
Prima di procedere con l’analisi delle funzioni specifiche, è importante installare correttamente Scapy:
pip install scapy
Su sistemi Linux potrebbe essere necessario eseguire gli script con privilegi di amministratore (sudo) per accedere alle interfacce di rete a basso livello.
Prima di immergerci nei dettagli di Ether, è fondamentale comprendere dove si colloca questo concetto nell’architettura delle reti. Le reti informatiche sono organizzate secondo un modello a strati (o “layer”), dove ogni strato fornisce servizi specifici allo strato superiore e si basa sui servizi forniti dallo strato inferiore. I due modelli di riferimento principali sono il modello OSI (7 strati) e il modello TCP/IP (4 strati).
Modello OSI semplificato:
Il livello Data Link (livello 2) è responsabile del trasferimento affidabile di dati tra due nodi direttamente connessi nella stessa rete locale. “Direttamente connessi” significa che non c’è un router tra loro - potrebbero esserci switch o hub, ma questi operano proprio al livello 2 e sono quindi “trasparenti” dal punto di vista del Data Link.
Ether è la classe di Scapy che rappresenta un frame Ethernet, ovvero l’unità di dati fondamentale del protocollo Ethernet che opera al livello 2 del modello OSI. Ma cosa significa esattamente “frame Ethernet”?
Immaginiamo Ethernet come un servizio postale all’interno di un edificio (la vostra LAN - Local Area Network). Quando volete inviare un documento a un collega nell’ufficio accanto, non spedite una lettera con francobollo tramite la posta nazionale - usate la posta interna dell’edificio. Mettete il documento in una busta interna, scrivete l’ufficio del destinatario, e lo consegnate al servizio di smistamento interno (lo switch Ethernet).
Il frame Ethernet è proprio questa “busta interna”: un contenitore standardizzato che permette ai dati di viaggiare all’interno della rete locale. Come una busta fisica, ha dei campi specifici dove vengono scritte informazioni cruciali per la consegna.
Ethernet fu inventato da Robert Metcalfe e David Boggs alla Xerox PARC negli anni '70. Inizialmente era un sistema di rete semplice progettato per collegare computer nello stesso edificio. Da allora, Ethernet si è evoluto enormemente:
Nonostante queste evoluzioni enormi in termini di velocità e mezzo fisico, la struttura base del frame Ethernet è rimasta sostanzialmente la stessa, il che è una testimonianza dell’eccellente design iniziale. Questo significa che i concetti che impariamo oggi sono validi sia per una vecchia rete Ethernet a 10 Mbps sia per moderne reti da 100 Gbps.
Un frame Ethernet è composto da diversi campi, ognuno con uno scopo specifico. Analizziamoli in dettaglio:
1. Preambolo e SFD (Start Frame Delimiter)
Il preambolo è come il countdown prima del lancio di un razzo: permette al dispositivo ricevente di “agganciarsi” al segnale elettrico in arrivo e prepararsi a ricevere i dati veri e propri. È un po’ come quando un’orchestra accorda gli strumenti prima di iniziare a suonare.
2. Indirizzo MAC di Destinazione (dst)
aa:bb:cc:dd:ee:ffL’indirizzo MAC (Media Access Control) è come il numero di appartamento nel nostro edificio. È un identificatore univoco assegnato a ogni scheda di rete al momento della fabbricazione. I primi 3 byte identificano il produttore (chiamato OUI - Organizationally Unique Identifier), mentre gli ultimi 3 byte sono assegnati dal produttore stesso per rendere l’indirizzo unico.
Curiosità: teoricamente, non dovrebbero esistere due schede di rete al mondo con lo stesso MAC address (anche se in pratica questo può essere aggirato via software).
Tipi di indirizzamento MAC:
Unicast: Destinato a un singolo dispositivo specifico (primo byte pari)
00:1A:2B:3C:4D:5EBroadcast: Destinato a TUTTI i dispositivi nella LAN
ff:ff:ff:ff:ff:ffMulticast: Destinato a un gruppo specifico di dispositivi (primo byte dispari)
01:00:5E:00:00:003. Indirizzo MAC Sorgente (src)
11:22:33:44:55:66È fondamentale che il mittente includa il proprio indirizzo MAC, altrimenti il destinatario non saprebbe a chi rispondere. È come mettere il mittente su una lettera.
4. EtherType / Lunghezza (type)
Valori EtherType comuni:
| Valore Esadecimale | Valore Decimale | Protocollo |
|---|---|---|
| 0x0800 | 2048 | IPv4 (Internet Protocol versione 4) |
| 0x0806 | 2054 | ARP (Address Resolution Protocol) |
| 0x86DD | 34525 | IPv6 (Internet Protocol versione 6) |
| 0x8100 | 33024 | VLAN tagging (802.1Q) |
| 0x88CC | 35020 | LLDP (Link Layer Discovery Protocol) |
| 0x8863 | 34915 | PPPoE Discovery |
| 0x8864 | 34916 | PPPoE Session |
Questo campo è cruciale perché dice al dispositivo ricevente: “Ho finito di leggere l’header Ethernet, ora i dati che seguono sono di tipo X, quindi passali al modulo appropriato per gestirli”. È come le etichette sui pacchi: “Fragile”, “Refrigerare”, ecc.
5. Payload (Dati)
Il payload è il “contenuto della busta” - ciò che effettivamente vogliamo trasportare. Potrebbe essere un pacchetto IP, una richiesta ARP, un frame di protocolli industriali come Modbus, ecc.
La limitazione a 1500 byte è chiamata MTU (Maximum Transmission Unit) di Ethernet. Se avete dati più grandi, devono essere frammentati in frame multipli. Pensatela come il limite di peso per un pacco postale: se superate il limite, dovete usare più pacchi.
6. Frame Check Sequence (FCS)
L’FCS è un codice di controllo errori. Il trasmittente calcola un valore basato su tutto il frame, e lo aggiunge alla fine. Il ricevente ricalcola questo valore: se corrisponde, il frame è integro; se non corrisponde, il frame è corrotto e viene scartato. È come un checksum, ma molto più robusto.
Importante: In Scapy, normalmente non gestiamo direttamente l’FCS perché è gestito dall’hardware della scheda di rete.
Ora che abbiamo compreso la teoria, vediamo come Scapy ci permette di lavorare concretamente con i frame Ethernet.
Creare un frame Ethernet basilare:
from scapy.all import Ether
# Creare un frame Ethernet con valori di default
frame = Ether()
# Scapy imposta automaticamente alcuni valori di default
print(frame.summary())
Quando creiamo un oggetto Ether() senza specificare parametri, Scapy applica delle scelte di default intelligenti. Possiamo visualizzare tutti i campi con il metodo show():
frame.show()
Output tipico:
###[ Ethernet ]###
dst = ff:ff:ff:ff:ff:ff
src = 00:00:00:00:00:00
type = 0x9000
Analizziamo questi valori:
dst = ff:ff:ff:ff:ff:ff: Scapy imposta il destinatario su broadcast. Questo significa che il frame sarà ricevuto da tutti i dispositivi nella LAN. È una scelta di default sensata perché spesso si usa Scapy per fare discovery o broadcasting.
src = 00:00:00:00:00:00: L’indirizzo sorgente viene lasciato a zero. Quando effettivamente invieremo questo frame sulla rete, il driver della scheda di rete sostituirà automaticamente questo valore con il vero MAC address della nostra interfaccia. Questo è comodo perché non dobbiamo preoccuparci di inserire manualmente il nostro MAC address.
type = 0x9000: Questo è un valore arbitrario che Scapy usa quando non c’è un payload specificato. Nella pratica reale, questo valore sarà sostituito da quello appropriato quando impileremo altri protocolli.
Possiamo specificare manualmente ogni campo del frame Ethernet per avere il controllo totale:
from scapy.all import Ether
# Creare un frame con destinazione specifica
frame_unicast = Ether(dst="aa:bb:cc:dd:ee:ff")
print(f"Questo frame è destinato a: {frame_unicast.dst}")
# Creare un frame specificando sia source che destination
frame_custom = Ether(
dst="00:1a:2b:3c:4d:5e", # MAC destinatario
src="00:11:22:33:44:55" # MAC mittente (possiamo fare spoofing!)
)
# Visualizzare la struttura completa
frame_custom.show()
Quando e perché personalizzare questi campi?
Scenario 1: Comunicazione Unicast Diretta
Se conosciamo il MAC address di un dispositivo specifico e vogliamo comunicare solo con lui (senza disturbare gli altri dispositivi della rete), impostiamo un indirizzo unicast:
# Voglio parlare solo con il dispositivo che ha questo MAC
router_mac = "00:1a:2b:3c:4d:5e"
frame = Ether(dst=router_mac)
Scenario 2: MAC Spoofing per Testing
In scenari di testing controllati, potremmo voler “fingere” di essere un altro dispositivo:
# Simulo di essere un dispositivo con un MAC specifico
# ATTENZIONE: usare solo in ambienti di test controllati!
frame = Ether(src="aa:bb:cc:dd:ee:ff", dst="00:1a:2b:3c:4d:5e")
Questo è utile per:
Scenario 3: Broadcast per Discovery
Quando vogliamo che tutti i dispositivi ricevano il nostro frame (tipico per ARP o discovery protocols):
# Messaggio broadcast
frame = Ether(dst="ff:ff:ff:ff:ff:ff")
Uno dei concetti più potenti e eleganti di Scapy è l’incapsulamento dei protocolli usando l’operatore /. Questo riflette esattamente come funzionano le reti nella realtà: ogni livello “impacchetta” il livello superiore come un suo payload.
Pensate a una bambola matrioska russa: ogni bambola contiene una bambola più piccola. Allo stesso modo, un frame Ethernet contiene un pacchetto IP, che contiene un segmento TCP, che contiene i dati dell’applicazione.
Esempio base: Ethernet + IP
from scapy.all import Ether, IP
# Creiamo un frame Ethernet che trasporta un pacchetto IP
packet = Ether() / IP(dst="192.168.1.1")
# Visualizziamo la struttura completa
packet.show()
Output:
###[ Ethernet ]###
dst = ff:ff:ff:ff:ff:ff
src = 00:00:00:00:00:00
type = 0x800 # <-- Nota: automaticamente impostato a IPv4!
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = hopopt
chksum = None
src = 192.168.1.10 # <-- Il nostro IP (automatico)
dst = 192.168.1.1 # <-- L'IP di destinazione che abbiamo specificato
\options \
Notate cosa è successo di magico: quando abbiamo impilato IP sopra Ether, Scapy ha automaticamente impostato il campo type di Ethernet a 0x800 (che indica IPv4). Scapy è abbastanza intelligente da comprendere le relazioni tra protocolli e configurare automaticamente i campi appropriati.
Esempio più complesso: Stack completo
from scapy.all import Ether, IP, TCP
# Costruiamo un pacchetto completo: Ethernet -> IP -> TCP
complete_packet = Ether() / IP(dst="192.168.1.100") / TCP(dport=80, sport=12345)
# Questo rappresenta una richiesta HTTP (porta 80)
# proveniente dalla porta locale 12345
Questo singolo oggetto rappresenta uno stack protocollare completo:
Quando creiamo pacchetti multi-layer, Scapy ci permette di accedere e modificare ogni singolo layer in modo indipendente:
from scapy.all import Ether, IP, TCP
# Creiamo un pacchetto complesso
packet = Ether() / IP(dst="192.168.1.100") / TCP(dport=80)
# Accedere al layer Ethernet
eth_layer = packet[Ether]
print(f"MAC destinazione: {eth_layer.dst}")
# Accedere al layer IP
ip_layer = packet[IP]
print(f"IP destinazione: {ip_layer.dst}")
print(f"TTL: {ip_layer.ttl}")
# Accedere al layer TCP
tcp_layer = packet[TCP]
print(f"Porta destinazione: {tcp_layer.dport}")
print(f"Porta sorgente: {tcp_layer.sport}")
# Modificare un campo specifico
packet[IP].ttl = 32 # Cambiamo il Time To Live
packet[TCP].flags = "S" # Impostiamo il flag SYN
Questo meccanismo di accesso è estremamente potente perché ci permette di navigare attraverso lo stack protocollare come se fosse un oggetto Python normale.
Vediamo alcuni scenari del mondo reale dove la manipolazione diretta dei frame Ethernet è cruciale:
1. Wake-on-LAN (WoL)
Wake-on-LAN è una tecnologia che permette di “risvegliare” computer spenti da remoto. Funziona inviando un “magic packet” - un frame Ethernet speciale contenente il MAC address del computer target ripetuto 16 volte.
from scapy.all import Ether, Raw
# MAC address del computer da risvegliare
target_mac = "00:1a:2b:3c:4d:5e"
# Costruiamo il magic packet
# Deve contenere 6 byte di 0xFF seguiti dal MAC ripetuto 16 volte
magic = b'\xff' * 6 + bytes.fromhex(target_mac.replace(':', '')) * 16
# Creiamo il frame Ethernet
wol_packet = Ether(dst="ff:ff:ff:ff:ff:ff") / Raw(load=magic)
# Inviamo il pacchetto (il computer si accenderà!)
# sendp(wol_packet, iface="eth0")
2. VLAN Tagging (802.1Q)
In reti enterprise, le VLAN (Virtual LAN) permettono di segmentare logicamente una rete fisica. Questo richiede l’aggiunta di un tag VLAN al frame Ethernet:
from scapy.all import Ether, Dot1Q, IP
# Pacchetto su VLAN 10
packet_vlan = Ether() / Dot1Q(vlan=10) / IP(dst="192.168.10.1")
# Scapy inserisce automaticamente il tag 0x8100 nell'EtherType
packet_vlan.show()
3. Analisi di traffico locale per troubleshooting
Quando una comunicazione non funziona in una LAN, spesso il problema è al livello 2. Possiamo usare Ether per costruire frame di test:
from scapy.all import Ether, ARP, srp
# Test: il dispositivo 192.168.1.50 risponde?
test_frame = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.50")
answered, unanswered = srp(test_frame, timeout=2, verbose=0)
if answered:
print(f"Dispositivo trovato! MAC: {answered[0][1].hwsrc}")
else:
print("Dispositivo non risponde - possibile problema di rete")
4. Testing di switch: MAC address table learning
Gli switch Ethernet imparano quali MAC address sono su quali porte osservando il traffico. Possiamo testare questo comportamento:
from scapy.all import Ether, sendp
# Inviamo frame con MAC sorgente diversi
for i in range(5):
fake_mac = f"00:11:22:33:44:{i:02x}"
test_frame = Ether(src=fake_mac, dst="ff:ff:ff:ff:ff:ff")
# sendp(test_frame, iface="eth0")
print(f"Inviato frame da MAC: {fake_mac}")
# Lo switch dovrebbe ora avere 5 entry diverse nella sua MAC address table
Per comprendere veramente ARP (Address Resolution Protocol), dobbiamo prima capire il problema che risolve. Immaginate di essere in una grande azienda. Voi conoscete il nome di un collega (“Mario Rossi, reparto vendite”) e volete inviargli un documento cartaceo. Ma per consegnare fisicamente il documento, avete bisogno di sapere in quale ufficio si trova esattamente (“Edificio B, piano 3, ufficio 307”).
Nelle reti informatiche succede qualcosa di molto simile:
Quando il vostro computer vuole inviare dati a un altro computer, il protocollo IP sa quale indirizzo IP raggiungere (ad esempio, 192.168.1.20), ma il livello Ethernet ha bisogno dell’indirizzo MAC per costruire il frame e consegnare fisicamente i bit sulla rete locale.
Il problema: Come facciamo a scoprire l’indirizzo MAC corrispondente a un determinato indirizzo IP?
La soluzione: ARP - Address Resolution Protocol!
ARP è essenzialmente un protocollo di domanda-e-risposta che opera tramite broadcast. Il processo è sorprendentemente semplice ed elegante. Vediamolo passo per passo con un esempio concreto:
Scenario iniziale:
Passo 1: ARP Request (Richiesta broadcast)
Computer A crea e invia un pacchetto ARP Request:
"A TUTTI i dispositivi di questa rete:
Chi tra voi ha l'indirizzo IP 192.168.1.20?
Se sei tu, per favore rispondimi dicendomi il tuo indirizzo MAC!
Il mio IP è 192.168.1.10 e il mio MAC è aa:bb:cc:dd:ee:ff"
Questo messaggio viene inviato in broadcast (a tutti), il che significa che:
Passo 2: Ricezione ed elaborazione
Ogni dispositivo sulla rete riceve il broadcast:
Passo 3: ARP Reply (Risposta unicast)
Computer B invia una risposta diretta (unicast) a Computer A:
"Ciao Computer A (aa:bb:cc:dd:ee:ff),
sono io che ho l'IP 192.168.1.20!
Il mio indirizzo MAC è 11:22:33:44:55:66.
Ora puoi inviarmi i dati direttamente!"
Questa risposta:
Passo 4: Aggiornamento ARP Cache
Computer A riceve la risposta e:
Da questo momento in poi, Computer A non ha più bisogno di fare ARP requests per 192.168.1.20 (almeno finché l’entry non scade dalla cache, tipicamente dopo alcuni minuti).
Un pacchetto ARP è strutturato in modo molto specifico. Comprendere questa struttura è fondamentale per usare efficacemente Scapy. Analizziamo ogni campo in dettaglio:
1. Hardware Type (hwtype)
Questo campo esiste perché ARP non è limitato solo a Ethernet. Può funzionare anche su altre tecnologie di rete (anche se nella pratica moderna, Ethernet domina).
2. Protocol Type (ptype)
Questo campo dice “sto risolvendo indirizzi di quale protocollo”. Nella pratica è quasi sempre IPv4.
3. Hardware Address Length (hwlen)
Per Ethernet è sempre 6 (perché i MAC sono 6 byte), ma per altre tecnologie potrebbe essere diverso.
4. Protocol Address Length (plen)
Per IPv4 è sempre 4 byte. Per IPv6 sarebbe 16 byte.
5. Operation (op)
Questo è il campo più importante perché distingue tra domanda e risposta.
6. Sender Hardware Address (hwsrc)
In una request, è il MAC di chi sta chiedendo. In una reply, è il MAC di chi sta rispondendo.
7. Sender Protocol Address (psrc)
8. Target Hardware Address (hwdst)
Questo campo è interessante perché in una ARP request è tipicamente zero (è proprio l’informazione che stiamo cercando).
9. Target Protocol Address (pdst)
In una request, è l’IP di cui vogliamo scoprire il MAC. In una reply, è l’IP del richiedente.
Vediamo come appare un vero pacchetto ARP in Scapy:
from scapy.all import ARP
# Creare una ARP request basilare
arp_request = ARP()
arp_request.show()
Output dettagliato:
###[ ARP ]###
hwtype = 0x1 # Hardware type: Ethernet
ptype = 0x800 # Protocol type: IPv4
hwlen = 6 # Hardware address length: 6 bytes (MAC)
plen = 4 # Protocol address length: 4 bytes (IPv4)
op = who-has # Operation: ARP request (value = 1)
hwsrc = 00:00:00:00:00:00 # Hardware source: vuoto (sarà riempito dalla NIC)
psrc = 0.0.0.0 # Protocol source: 0.0.0.0 (sarà riempito automaticamente)
hwdst = 00:00:00:00:00:00 # Hardware dest: vuoto (è quello che cerchiamo!)
pdst = 0.0.0.0 # Protocol dest: non specificato
Analizziamo questo output:
op = who-has: Scapy mostra “who-has” invece di “1” per maggiore leggibilitàhwsrc e psrc sono a zero ma verranno riempiti automaticamente quando invieremo il pacchettohwdst è zero perché è esattamente l’informazione che stiamo cercandopdst dovremo specificarlo noi per dire “quale IP stiamo cercando”Ora creiamo una vera ARP request che potremmo effettivamente inviare sulla rete:
from scapy.all import Ether, ARP
# Definiamo l'IP di cui vogliamo scoprire il MAC
target_ip = "192.168.1.1" # Tipicamente il gateway/router
# STEP 1: Creiamo il layer ARP
arp_layer = ARP(pdst=target_ip)
# STEP 2: Creiamo il layer Ethernet (deve essere broadcast)
ether_layer = Ether(dst="ff:ff:ff:ff:ff:ff")
# STEP 3: Combiniamo i due layer
complete_arp_request = ether_layer / arp_layer
# Visualizziamo il pacchetto completo
print("=== PACCHETTO ARP REQUEST COMPLETO ===")
complete_arp_request.show()
Output:
=== PACCHETTO ARP REQUEST COMPLETO ===
###[ Ethernet ]###
dst = ff:ff:ff:ff:ff:ff # Broadcast
src = 00:00:00:00:00:00 # Sarà riempito dalla NIC
type = 0x806 # ARP (Scapy lo ha impostato automaticamente!)
###[ ARP ]###
hwtype = 0x1
ptype = 0x800
hwlen = 6
plen = 4
op = who-has # Request
hwsrc = 00:00:00:00:00:00
psrc = 0.0.0.0 # Sarà riempito con il nostro IP
hwdst = 00:00:00:00:00:00 # È quello che cerchiamo
pdst = 192.168.1.1 # L'IP target che vogliamo risolvere
Notate come Scapy abbia automaticamente impostato type = 0x806 nel layer Ethernet. Questo è il codice per ARP, e Scapy lo ha dedotto dal fatto che abbiamo impilato un layer ARP sopra Ethernet.
Quando invieremo questo pacchetto sulla rete (usando srp che vedremo più avanti), ecco cosa accadrà:
Il nostro computer:
hwsrc con il nostro MAC addresspsrc con il nostro IP addressLo switch:
ff:ff:ff:ff:ff:ff (broadcast)Tutti i dispositivi della LAN:
pdstpdst corrisponde al loro IP, preparano una rispostapdst NON corrisponde, scartano il pacchettoIl dispositivo con IP 192.168.1.1 (nel nostro esempio):
pdst = 192.168.1.1 (il suo IP!)Anche se normalmente non creeremo ARP replies manualmente (i dispositivi rispondono automaticamente), è istruttivo vedere come sono fatte:
from scapy.all import Ether, ARP
# Scenario: Computer B (192.168.1.20, MAC: aa:bb:cc:dd:ee:ff)
# risponde a Computer A (192.168.1.10, MAC: 11:22:33:44:55:66)
# STEP 1: Layer Ethernet (unicast, diretto a Computer A)
ether_reply = Ether(
dst="11:22:33:44:55:66", # MAC di Computer A (il richiedente)
src="aa:bb:cc:dd:ee:ff" # Il nostro MAC (Computer B)
)
# STEP 2: Layer ARP (reply)
arp_reply = ARP(
op=2, # 2 = is-at (ARP reply)
hwsrc="aa:bb:cc:dd:ee:ff", # Il nostro MAC
psrc="192.168.1.20", # Il nostro IP
hwdst="11:22:33:44:55:66", # MAC del richiedente
pdst="192.168.1.10" # IP del richiedente
)
# STEP 3: Pacchetto completo
complete_arp_reply = ether_reply / arp_reply
print("=== ARP REPLY ===")
complete_arp_reply.show()
Output:
=== ARP REPLY ===
###[ Ethernet ]###
dst = 11:22:33:44:55:66 # Unicast al richiedente
src = aa:bb:cc:dd:ee:ff # Noi
type = 0x806
###[ ARP ]###
hwtype = 0x1
ptype = 0x800
hwlen = 6
plen = 4
op = is-at # Reply (valore = 2)
hwsrc = aa:bb:cc:dd:ee:ff # Il nostro MAC
psrc = 192.168.1.20 # Il nostro IP
hwdst = 11:22:33:44:55:66 # MAC del richiedente (ora lo conosciamo)
pdst = 192.168.1.10 # IP del richiedente
La differenza chiave con la request:
op = is-at (valore 2) invece di who-has (valore 1)hwdst ora è pieno (conosciamo il MAC del richiedente)Una caratteristica fondamentale di ARP è la cache (o tabella ARP). Sarebbe estremamente inefficiente inviare una ARP request ogni singola volta che vogliamo comunicare con un dispositivo. Invece, i sistemi operativi mantengono una tabella che memorizza i mapping IP-MAC appresi.
Visualizzare la ARP cache:
Su Linux/Mac:
arp -a
Su Windows:
arp -a
Output tipico:
? (192.168.1.1) at 00:1a:2b:3c:4d:5e [ether] on eth0
? (192.168.1.20) at aa:bb:cc:dd:ee:ff [ether] on eth0
? (192.168.1.30) at 11:22:33:44:55:66 [ether] on eth0
Questo mostra:
Lifetime delle entry ARP:
Le entry nella ARP cache non rimangono per sempre. Tipicamente:
Questo meccanismo di scadenza è importante perché:
Manipolare la ARP cache da command line:
# Aggiungere una entry statica (Linux)
sudo arp -s 192.168.1.100 aa:bb:cc:dd:ee:ff
# Rimuovere una entry specifica (Linux)
sudo arp -d 192.168.1.100
# Svuotare completamente la cache (Linux)
sudo ip -s -s neigh flush all
# Windows - aggiungere entry statica
arp -s 192.168.1.100 aa-bb-cc-dd-ee-ff
# Windows - rimuovere entry
arp -d 192.168.1.100
Un caso speciale di ARP è il Gratuitous ARP (ARP gratuito). È un ARP request in cui:
psrc e pdst sono ugualiScopi del Gratuitous ARP:
Esempio di Gratuitous ARP con Scapy:
from scapy.all import Ether, ARP
# Gratuitous ARP: annuncio "Io sono 192.168.1.50 e ho questo MAC"
gratuitous = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(
op=1, # È una request
psrc="192.168.1.50", # Il mio IP
pdst="192.168.1.50", # Sto chiedendo il mio stesso IP
hwsrc="aa:bb:cc:dd:ee:ff" # Il mio MAC
)
# Quando inviamo questo, tutti i dispositivi aggiorneranno la loro cache
# con: 192.168.1.50 -> aa:bb:cc:dd:ee:ff
Prima di configurare un indirizzo IP, un dispositivo intelligente dovrebbe verificare che non sia già in uso. Questo si fa con un ARP Probe:
from scapy.all import Ether, ARP
# Vogliamo usare 192.168.1.100, ma è libero?
ip_to_check = "192.168.1.100"
# ARP Probe: psrc = 0.0.0.0, pdst = IP da verificare
probe = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(
op=1,
psrc="0.0.0.0", # Source IP è zero!
pdst=ip_to_check, # L'IP che vogliamo verificare
hwsrc="aa:bb:cc:dd:ee:ff" # Il nostro MAC
)
# Se qualcuno risponde, l'IP è già in uso
# Se nessuno risponde, l'IP è libero
La differenza chiave è psrc = 0.0.0.0 - questo dice “non ho ancora un IP, sto solo controllando”.
1. Network Discovery avanzato con statistiche:
from scapy.all import Ether, ARP, srp
import time
def advanced_arp_discovery(network_range):
"""
Scansione ARP con raccolta di statistiche dettagliate
"""
print(f"Scansione ARP di {network_range}...")
# Costruzione pacchetto
arp = ARP(pdst=network_range)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether / arp
# Timing
start_time = time.time()
# Invio e ricezione
answered, unanswered = srp(packet, timeout=3, verbose=0)
end_time = time.time()
elapsed = end_time - start_time
# Statistiche
total_ips = 254 # Per una /24
found = len(answered)
missing = len(unanswered)
response_rate = (found / total_ips) * 100
print(f"\n{'='*60}")
print(f"RISULTATI SCANSIONE ARP")
print(f"{'='*60}")
print(f"Tempo impiegato: {elapsed:.2f} secondi")
print(f"Host trovati: {found}")
print(f"Host non raggiungibili: {missing}")
print(f"Tasso di risposta: {response_rate:.1f}%")
print(f"\n{'IP Address':<20} {'MAC Address':<20} {'Vendor OUI'}")
print(f"{'-'*60}")
for sent, received in answered:
# I primi 3 byte del MAC identificano il vendor
vendor_oui = received.hwsrc[:8] # XX:XX:XX
print(f"{received.psrc:<20} {received.hwsrc:<20} {vendor_oui}")
return answered, unanswered
# Uso
# results = advanced_arp_discovery("192.168.1.0/24")
2. Rilevamento di ARP Spoofing/Poisoning:
ARP può essere abusato per attacchi di tipo “man-in-the-middle”. Possiamo creare uno script di monitoraggio:
from scapy.all import Ether, ARP
# Definiamo l'IP target di cui vogliamo scoprire il MAC
target_ip = "192.168.1.1"
# Creiamo il frame Ethernet (broadcast)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
# Creiamo la ARP request
arp = ARP(pdst=target_ip)
# Combiniamo i due livelli
packet = ether/arp
packet.show()
Questo pacchetto, quando inviato, chiederà: “Chi ha l’indirizzo IP 192.168.1.1? Rispondi al mio MAC address con il tuo!”
Possiamo anche creare manualmente una risposta ARP (utile per testing o per comprendere attacchi come ARP poisoning):
arp_reply = ARP(
op=2, # 2 = is-at (ARP reply)
hwsrc="aa:bb:cc:dd:ee:ff", # Il nostro MAC
psrc="192.168.1.100", # Il nostro IP
hwdst="11:22:33:44:55:66", # MAC del richiedente
pdst="192.168.1.50" # IP del richiedente
)
I sistemi operativi mantengono una ARP cache (tabella ARP) che memorizza le associazioni IP-MAC apprese. Questo evita di dover inviare una ARP request ogni volta che si comunica con lo stesso host.
Su Linux puoi visualizzare la cache ARP con:
arp -a
Su Windows:
arp -a
from scapy.all import Ether, ARP, srp
# Definiamo il range di IP da scansionare
target_range = "192.168.1.0/24"
# Creiamo il pacchetto ARP per l'intera subnet
arp = ARP(pdst=target_range)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether/arp
# Inviamo e riceviamo (vedremo srp più avanti)
result = srp(packet, timeout=2, verbose=0)[0]
# Elaboriamo i risultati
for sent, received in result:
print(f"IP: {received.psrc} - MAC: {received.hwsrc}")
Questo script scansionerà tutti i 254 possibili indirizzi IP della subnet 192.168.1.0/24 e stamperà quali rispondono con il loro MAC address.
Prima di immergerci in hexdump, dobbiamo capire perché la rappresentazione esadecimale è così importante nell’informatica, e specialmente nelle reti.
I computer, a livello fondamentale, operano esclusivamente con bit - sequenze di 0 e 1. Ogni dato che viaggia su una rete, ogni file sul vostro disco, ogni istruzione eseguita dal processore, tutto è rappresentato come una serie di bit. Un pacchetto di rete, quindi, è letteralmente una sequenza di migliaia di bit che vengono trasmessi uno dopo l’altro sul mezzo fisico (cavo, fibra ottica, onde radio).
Il problema della visualizzazione:
Visualizzare i dati come sequenza pura di bit è praticamente impossibile per un essere umano:
11111111111111111111111111111111000000000000000000000000000000001000000001100000...
Questa stringa è incomprensibile. Serve un modo più pratico di rappresentare i dati binari.
Perché l’esadecimale?
Il sistema esadecimale (base 16) è perfetto per rappresentare dati binari perché:
Compattezza: Un singolo digit esadecimale rappresenta esattamente 4 bit
0000 binario = 0 esadecimale1111 binario = F esadecimaleLeggibilità: Molto più facile leggere FF A3 C1 5E che 11111111101000111100000101011110
Conversione diretta: Non serve fare calcoli complessi per convertire binario ↔ esadecimale
Standard industriale: Tutte le specifiche di protocolli di rete usano notazione esadecimale
Tabella di conversione binario-esadecimale:
| Decimale | Binario | Esadecimale |
|---|---|---|
| 0 | 0000 | 0 |
| 1 | 0001 | 1 |
| 2 | 0010 | 2 |
| 3 | 0011 | 3 |
| 4 | 0100 | 4 |
| 5 | 0101 | 5 |
| 6 | 0110 | 6 |
| 7 | 0111 | 7 |
| 8 | 1000 | 8 |
| 9 | 1001 | 9 |
| 10 | 1010 | A |
| 11 | 1011 | B |
| 12 | 1100 | C |
| 13 | 1101 | D |
| 14 | 1110 | E |
| 15 | 1111 | F |
hexdump è una funzione di utilità di Scapy che permette di visualizzare il contenuto grezzo (raw) di un pacchetto esattamente come viene trasmesso sulla rete, byte per byte, in formato esadecimale. È come avere i “raggi X” per i pacchetti di rete - potete vedere esattamente cosa c’è dentro.
Quando usate protocolli di alto livello (browser web, email, ecc.), il sistema operativo nasconde tutti questi dettagli. Con hexdump, invece, vedete la vera natura binaria della comunicazione di rete.
Quando chiamate hexdump su un pacchetto, ottenete un output strutturato in tre sezioni principali. Analizziamolo nel dettaglio:
from scapy.all import Ether, ARP, hexdump
# Creiamo un pacchetto ARP semplice
packet = Ether(dst="ff:ff:ff:ff:ff:ff", src="aa:bb:cc:dd:ee:ff") / ARP(pdst="192.168.1.1")
# Visualizziamo in esadecimale
hexdump(packet)
Output esempio:
0000 FF FF FF FF FF FF AA BB CC DD EE FF 08 06 00 01 ................
0010 08 00 06 04 00 01 AA BB CC DD EE FF 00 00 00 00 ................
0020 00 00 00 00 00 00 C0 A8 01 01 ..........
Analizziamo ogni componente di questo output:
1. COLONNA OFFSET (Prima colonna - 0000, 0010, 0020)
L’offset vi dice “a quanti byte dall’inizio del pacchetto mi trovo”. È come i numeri di riga in un documento - vi aiuta a navigare e a fare riferimento a posizioni specifiche.
Esempio:
0000: Byte da 0 a 150010: Byte da 16 (0x10) a 31 (0x1F)0020: Byte da 32 (0x20) a 47 (0x2F)2. COLONNA DATI ESADECIMALI (Colonne centrali)
Questa è la parte più importante - i veri dati del pacchetto. Ogni coppia di cifre rappresenta un byte.
3. COLONNA ASCII (Ultima colonna - tra i punti)
. per non stampabiliMolti byte non corrispondono a caratteri ASCII stampabili (valori < 32 o > 126), quindi vengono mostrati come . (punto).
Prendiamo l’output sopra e decodifichiamolo campo per campo, collegando ogni byte al suo significato nel protocollo:
0000 FF FF FF FF FF FF AA BB CC DD EE FF 08 06 00 01
└─────┬─────┘ └─────┬──┘ └─┬─┘ └─┬─┘ └─┬─┘
MAC dest MAC src EthType HwType
Byte 0-5: MAC Destination (FF FF FF FF FF FF)
Byte 6-11: MAC Source (AA BB CC DD EE FF)
Byte 12-13: EtherType (08 06)
0x0806 in esadecimale = 2054 in decimaleFin qui abbiamo il header Ethernet (14 byte totali). Ora inizia il pacchetto ARP:
0000 00 01
0010 08 00 06 04 00 01 AA BB CC DD EE FF 00 00 00 00
└─┬─┘ └┬┘└┬┘ └─┬─┘ └─────┬─────┘ └─────┬─────┘
PType HL PL Op HW Src Proto Src
Byte 14-15: Hardware Type (00 01)
0x0001 = 1 in decimaleByte 16-17: Protocol Type (08 00)
0x0800 = IPv4Byte 18: Hardware Length (06)
Byte 19: Protocol Length (04)
Byte 20-21: Operation (00 01)
0x0001 = 1 = ARP Request (“who-has”)Byte 22-27: Sender Hardware Address (AA BB CC DD EE FF)
Byte 28-31: Sender Protocol Address (00 00 00 00)
0020 00 00 00 00 00 00 C0 A8 01 01
└─────┬─────┘ └────┬────┘
HW Dest Proto Dest
Byte 32-37: Target Hardware Address (00 00 00 00 00 00)
Byte 38-41: Target Protocol Address (C0 A8 01 01)
È fondamentale saper convertire tra esadecimale e decimale. Vediamo i metodi:
Esadecimale → Decimale:
Metodo posizionale (per numeri multi-cifra):
C0A8 (esadecimale) = ?
C 0 A 8
12 0 10 8
= (12 × 16³) + (0 × 16²) + (10 × 16¹) + (8 × 16⁰)
= (12 × 4096) + (0 × 256) + (10 × 16) + (8 × 1)
= 49152 + 0 + 160 + 8
= 49320 (decimale)
Per byte singoli (più comune):
FF (esadecimale) = (15 × 16) + 15 = 255 (decimale)
C0 (esadecimale) = (12 × 16) + 0 = 192 (decimale)
A8 (esadecimale) = (10 × 16) + 8 = 168 (decimale)
Decimale → Esadecimale:
Metodo divisione per 16:
192 (decimale) = ? (esadecimale)
192 ÷ 16 = 12 resto 0
12 in esadecimale = C
0 in esadecimale = 0
Quindi: 192 = C0
Usando Python per verificare:
# Decimale → Esadecimale
decimal_value = 192
hex_value = hex(decimal_value)
print(hex_value) # Output: 0xc0
# Esadecimale → Decimale
hex_value = "C0"
decimal_value = int(hex_value, 16)
print(decimal_value) # Output: 192
# Per un indirizzo IP completo
ip_bytes = [192, 168, 1, 1]
hex_ip = ' '.join([f"{byte:02X}" for byte in ip_bytes])
print(hex_ip) # Output: C0 A8 01 01
Scapy offre due modi principali per visualizzare pacchetti, e capire quando usare l’uno o l’altro è importante:
show() - Vista “interpretata”:
packet.show()
Vantaggi:
Usa quando:
hexdump() - Vista “grezza”:
hexdump(packet)
Vantaggi:
Usa quando:
Esempio comparativo:
from scapy.all import Ether, IP, TCP, hexdump
# Pacchetto TCP con payload
packet = Ether() / IP(dst="192.168.1.100") / TCP(dport=80, flags="S") / "Hello"
print("=== VISTA INTERPRETATA (show) ===")
packet.show()
print("\n=== VISTA GREZZA (hexdump) ===")
hexdump(packet)
La vista show() dirà “c’è un TCP SYN verso la porta 80 con payload ‘Hello’”.
La vista hexdump() mostrerà esattamente come questi dati sono codificati in byte.
1. Verificare padding e allineamento
I protocolli di rete spesso richiedono padding per allineamento. hexdump lo rivela:
from scapy.all import Ether, ARP, hexdump
# ARP con dati molto piccoli
small_arp = Ether() / ARP(pdst="192.168.1.1")
hexdump(small_arp)
# Potreste vedere byte di padding alla fine se il frame è < 64 byte
# (minimo per Ethernet)
2. Debugging di problemi di byte order (endianness)
Alcuni protocolli usano big-endian, altri little-endian:
# Valore 1234 in big-endian (network byte order)
big_endian = b'\x04\xd2' # 04 = 4, D2 = 210 → (4 × 256) + 210 = 1234
# Valore 1234 in little-endian
little_endian = b'\xd2\x04' # D2 = 210, 04 = 4 → (210 + 4 × 256) = 1234
# hexdump rivela immediatamente l'ordine dei byte
from scapy.all import hexdump, Raw
hexdump(Raw(load=big_endian))
# Output: 0000 04 D2 ..
hexdump(Raw(load=little_endian))
# Output: 0000 D2 04 ..
3. Identificare stringhe nascoste in pacchetti
A volte i payload contengono stringhe di testo. La colonna ASCII di hexdump le rende visibili:
from scapy.all import IP, TCP, hexdump
# HTTP request nascosta in un pacchetto
http_packet = IP() / TCP() / "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
hexdump(http_packet)
# La colonna ASCII mostrerà "GET / HTTP/1.1" chiaramente visibile
4. Reverse engineering di protocolli sconosciuti
Se intercettate traffico di un protocollo proprietario, hexdump è il primo strumento:
from scapy.all import hexdump, sniff
# Catturate alcuni pacchetti
captured = sniff(count=1, iface="eth0")
# Analizzate la struttura byte per byte
hexdump(captured[0])
# Cercate pattern ripetuti, valori fissi (magic numbers),
# lunghezze, checksum position, ecc.
5. Confrontare pacchetti per trovare differenze
from scapy.all import Ether, ARP, hexdump
# Due pacchetti quasi identici
packet1 = Ether() / ARP(pdst="192.168.1.1")
packet2 = Ether() / ARP(pdst="192.168.1.2")
print("=== PACCHETTO 1 ===")
hexdump(packet1)
print("\n=== PACCHETTO 2 ===")
hexdump(packet2)
# Confrontando, vedrete che cambiano solo gli ultimi byte (l'ultimo byte dell'IP)
A volte volete salvare l’output di hexdump per documentazione o report:
from scapy.all import Ether, ARP, hexdump
import sys
packet = Ether() / ARP(pdst="192.168.1.1")
# Salvare in un file
with open('packet_analysis.txt', 'w') as f:
# Redirigere stdout temporaneamente
old_stdout = sys.stdout
sys.stdout = f
hexdump(packet)
sys.stdout = old_stdout
print("Hexdump salvato in packet_analysis.txt")
Quando fate analisi di pacchetti, hexdump si integra bene con altri strumenti:
1. bytes() - Ottenere i byte grezzi
from scapy.all import Ether, ARP
packet = Ether() / ARP(pdst="192.168.1.1")
# Ottenere i byte come oggetto Python bytes
raw_bytes = bytes(packet)
print(f"Lunghezza totale: {len(raw_bytes)} byte")
print(f"Primi 10 byte: {raw_bytes[:10]}")
# Ogni elemento è un byte (0-255)
for i, byte in enumerate(raw_bytes[:20]):
print(f"Byte {i}: {byte:3d} (decimale) = {byte:02X} (hex) = {bin(byte)} (binario)")
2. raw() - Simile a bytes()
from scapy.all import Ether, ARP, raw
packet = Ether() / ARP(pdst="192.168.1.1")
# raw() è equivalente a bytes()
raw_data = raw(packet)
3. sprintf() - Formattazione custom
from scapy.all import Ether, ARP, IP
packet = Ether() / IP(dst="192.168.1.100") / ARP(pdst="192.168.1.1")
# Formattazione personalizzata
formatted = packet.sprintf("Ethernet: %Ether.src% -> %Ether.dst% | IP dst: %IP.dst%")
print(formatted)
Esercizio 1: Decodifica manuale
# Guardate questo hexdump e identificate:
# - Il tipo di pacchetto
# - Gli indirizzi MAC
# - L'indirizzo IP target
"""
0000 FF FF FF FF FF FF 11 22 33 44 55 66 08 06 00 01
0010 08 00 06 04 00 01 11 22 33 44 55 66 C0 A8 01 0A
0020 00 00 00 00 00 00 C0 A8 01 01
"""
# Risposta:
# - Pacchetto: ARP request (08 06 = EtherType ARP, 00 01 = Operation Request)
# - MAC destinazione: FF:FF:FF:FF:FF:FF (broadcast)
# - MAC sorgente: 11:22:33:44:55:66
# - IP sorgente: C0 A8 01 0A = 192.168.1.10
# - IP target: C0 A8 01 01 = 192.168.1.1
Esercizio 2: Trovare il magic number
Molti protocolli iniziano con un “magic number” - una sequenza fissa di byte che identifica il protocollo. Trovatelo:
from scapy.all import hexdump, Raw
# Un protocollo misterioso
mystery_protocol = Raw(load=b'\xCA\xFE\xBA\xBE' + b'\x00\x01\x02\x03' + b'Hello')
hexdump(mystery_protocol)
# Output: 0000 CA FE BA BE 00 01 02 03 Hello
# Magic number: CA FE BA BE (in realtà usato da Java class files!)
Finora abbiamo imparato a costruire pacchetti (con Ether e ARP), a visualizzarli in formato strutturato (show) e grezzo (hexdump). Ma nella rete reale, la comunicazione non è mai unidirezionale - è un dialogo. Quando inviate una richiesta ARP, vi aspettate una risposta. Quando fate un ping, vi aspettate un pong. Quando inviate un SYN, vi aspettate un SYN-ACK.
srp (Send and Receive Packets) è la funzione di Scapy che implementa questa comunicazione bidirezionale al livello 2 (Data Link layer). È come avere un walkie-talkie: premete il pulsante, parlate (send), rilasciate il pulsante, ascoltate (receive).
Scapy offre diverse varianti per inviare e ricevere pacchetti. Comprendere le differenze è fondamentale:
1. send() / recv() - Livello 3 (Network)
from scapy.all import IP, ICMP, send
# Invia un ping (ICMP) - routing automatico
packet = IP(dst="8.8.8.8") / ICMP()
send(packet) # Solo invio, nessuna ricezione
2. sendp() / sniff() - Livello 2 (Data Link)
from scapy.all import Ether, ARP, sendp
# Invia un frame Ethernet senza aspettare risposta
frame = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.1")
sendp(frame, iface="eth0") # Solo invio
3. sr() / sr1() - Livello 3 con ricezione
from scapy.all import IP, ICMP, sr1
# Invia ping e aspetta risposta
packet = IP(dst="192.168.1.1") / ICMP()
response = sr1(packet, timeout=2) # Ritorna solo la prima risposta
if response:
response.show()
4. srp() / srp1() - Livello 2 con ricezione ← IL NOSTRO FOCUS
from scapy.all import Ether, ARP, srp
# Invia ARP request e aspetta risposte
packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.0/24")
answered, unanswered = srp(packet, timeout=2) # Ritorna entrambe le liste
Tabella riassuntiva:
| Funzione | Livello | Invia | Riceve | Ritorna | Uso Principale |
|---|---|---|---|---|---|
| send() | 3 (IP) | ✓ | ✗ | - | Fire-and-forget IP |
| sendp() | 2 (Ethernet) | ✓ | ✗ | - | Fire-and-forget Ethernet |
| sr() | 3 (IP) | ✓ | ✓ | (answered, unanswered) | Dialogo IP |
| sr1() | 3 (IP) | ✓ | ✓ | prima risposta | Dialogo IP singolo |
| srp() | 2 (Ethernet) | ✓ | ✓ | (answered, unanswered) | Dialogo Ethernet |
| srp1() | 2 (Ethernet) | ✓ | ✓ | prima risposta | Dialogo Ethernet singolo |
Quando chiamate srp(), Scapy esegue una sequenza complessa di operazioni. Comprendere questi passaggi aiuta a usare la funzione in modo efficace:
FASE 1: PREPARAZIONE
answered, unanswered = srp(packet, timeout=2, verbose=1)
Scapy prende il vostro pacchetto (o lista di pacchetti) e prepara:
FASE 2: INVIO
Per ogni pacchetto da inviare:
1. Serializza il pacchetto in byte
2. Riempie i campi automatici (src MAC, checksums, ecc.)
3. Invia sul wire tramite socket raw
4. Memorizza timestamp di invio
5. Aggiunge a lista "waiting for answer"
Se avete specificato inter=0.1, Scapy aspetterà 100ms tra un invio e l’altro.
FASE 3: RICEZIONE (ASCOLTO)
Mentre timer < timeout:
1. Ascolta l'interfaccia di rete
2. Cattura ogni pacchetto che arriva
3. Verifica se il pacchetto è una risposta a una delle nostre richieste
4. Se sì:
- Rimuove dalla lista "waiting for answer"
- Aggiunge a "answered" come tupla (sent, received)
5. Se no:
- Ignora il pacchetto
Il meccanismo per “verificare se è una risposta” si basa sul fatto che Scapy controlla se i pacchetti sono correlati (stessa conversazione).
FASE 4: TIMEOUT E CLEANUP
Quando scade il timeout:
1. Smette di ascoltare
2. Tutti i pacchetti in "waiting for answer" diventano "unanswered"
3. Ritorna entrambe le liste
srp restituisce una tupla di due elementi. Analizziamo in dettaglio cosa contengono:
answered, unanswered = srp(packet, timeout=2)
1. answered - Lista di coppie (sent, received)
Tipo: SRPResult (si comporta come una lista di tuple)
Struttura:
[(packet_sent_1, packet_received_1),
(packet_sent_2, packet_received_2),
(packet_sent_3, packet_received_3),
...]
Ogni elemento è una tupla dove:
Iterazione su answered:
# Metodo 1: Unpacking diretto
for sent, received in answered:
print(f"Ho inviato: {sent.summary()}")
print(f"Ho ricevuto: {received.summary()}")
print(f"IP sorgente risposta: {received.psrc}")
print(f"MAC sorgente risposta: {received.hwsrc}")
print()
# Metodo 2: Accesso tramite indice
for i in range(len(answered)):
sent_packet = answered[i][0] # Il pacchetto inviato
received_packet = answered[i][1] # La risposta ricevuta
print(f"Coppia {i}: {sent_packet.summary()} -> {received_packet.summary()}")
# Metodo 3: Accedere a una specifica coppia
first_sent, first_received = answered[0] # Prima coppia
last_sent, last_received = answered[-1] # Ultima coppia
2. unanswered - Lista di pacchetti senza risposta
Tipo: PacketList
Struttura:
[packet_1, packet_2, packet_3, ...]
Sono i pacchetti che abbiamo inviato ma per i quali NON abbiamo ricevuto risposta entro il timeout.
Iterazione su unanswered:
for packet in unanswered:
print(f"Nessuna risposta per: {packet.summary()}")
# Possiamo accedere ai campi del pacchetto
if ARP in packet:
print(f" Target IP era: {packet[ARP].pdst}")
srp accetta numerosi parametri che permettono un controllo granulare. Analizziamoli tutti:
srp(
x, # Pacchetto(i) da inviare
timeout=2, # Timeout in secondi
iface=None, # Interfaccia di rete
inter=0, # Intervallo tra pacchetti
verbose=None, # Livello di verbosità
chainCC=False, # Gestione Ctrl+C
retry=0, # Numero di retry
multi=False, # Accettare risposte multiple
filter=None, # Filtro BPF custom
promisc=None, # Modalità promiscua
store=True, # Memorizzare pacchetti
**kargs # Altri parametri
)
1. x - Pacchetto(i) da inviare
Può essere:
Ether() / ARP(pdst="192.168.1.1")[packet1, packet2, packet3]# Singolo pacchetto
single = Ether() / ARP(pdst="192.168.1.1")
srp(single, timeout=2)
# Lista di pacchetti
packet_list = [
Ether() / ARP(pdst="192.168.1.1"),
Ether() / ARP(pdst="192.168.1.2"),
Ether() / ARP(pdst="192.168.1.3")
]
srp(packet_list, timeout=2)
2. timeout - Tempo massimo di attesa
# Timeout breve per rete locale veloce
srp(packet, timeout=0.5)
# Timeout lungo per rete lenta o remota
srp(packet, timeout=5)
# Nessun timeout (aspetta indefinitamente - PERICOLOSO!)
srp(packet, timeout=None)
Scegliere il timeout giusto:
3. iface - Interfaccia di rete
# Visualizzare interfacce disponibili
from scapy.all import get_if_list, conf
print(get_if_list()) # Lista tutte le interfacce
print(conf.iface) # Interfaccia di default
# Usare interfaccia specifica
srp(packet, iface="eth0", timeout=2)
srp(packet, iface="wlan0", timeout=2)
Perché specificare l’interfaccia:
4. inter - Intervallo tra pacchetti
# Invia immediatamente (default)
srp(packet_list, inter=0, timeout=2)
# 100ms tra ogni pacchetto
srp(packet_list, inter=0.1, timeout=2) # 10 pacchetti/secondo
# 1 secondo tra ogni pacchetto
srp(packet_list, inter=1, timeout=5) # 1 pacchetto/secondo
Quando usare inter:
5. verbose - Livello di output
# Silenzioso - nessun output
answered, unanswered = srp(packet, verbose=0, timeout=2)
# Normale - mostra riepilogo
answered, unanswered = srp(packet, verbose=1, timeout=2)
# Output: "Begin emission: ... Finished sending ... Received X packets"
# Dettagliato - mostra ogni pacchetto
answered, unanswered = srp(packet, verbose=2, timeout=2)
# Mostra ogni invio e ricezione in tempo reale
6. retry - Numero di tentativi
# Nessun retry (default)
srp(packet, retry=0, timeout=2)
# 2 retry (3 invii totali per ogni pacchetto)
srp(packet, retry=2, timeout=2)
Come funziona retry:
Invio 1: [packet1] [packet2] [packet3]
Aspetta timeout...
packet2 non ha risposto
Invio 2 (retry 1): [packet2]
Aspetta timeout...
packet2 ancora non risponde
Invio 3 (retry 2): [packet2]
Aspetta timeout...
packet2 → unanswered
7. multi - Risposte multiple
# Default: Solo la prima risposta per pacchetto
answered, unanswered = srp(packet, multi=False, timeout=2)
len(answered) # Massimo N (quanti pacchetti inviati)
# Multi: Accetta tutte le risposte
answered, unanswered = srp(packet, multi=True, timeout=2)
len(answered) # Può essere > N (se più dispositivi rispondono)
Quando usare multi=True:
Mettiamo insieme tutto quello che abbiamo imparato in uno script completo e commentato:
from scapy.all import Ether, ARP, srp
import time
def comprehensive_arp_scan(target_range, timeout=3, inter=0.05):
"""
Scansione ARP completa con analisi dettagliata
Args:
target_range (str): Range IP in notazione CIDR (es. "192.168.1.0/24")
timeout (int): Secondi di attesa per le risposte
inter (float): Secondi tra l'invio di pacchetti successivi
Returns:
tuple: (answered, unanswered, statistics)
"""
print(f"\n{'='*70}")
print(f"SCANSIONE ARP AVANZATA")
print(f"{'='*70}")
print(f"Target Range: {target_range}")
print(f"Timeout: {timeout}s")
print(f"Intervallo tra pacchetti: {inter}s")
print(f"{'='*70}\n")
# ===== FASE 1: COSTRUZIONE PACCHETTO =====
print("[FASE 1] Costruzione pacchetto ARP...")
# Layer Ethernet: broadcast per raggiungere tutti
ether_layer = Ether(dst="ff:ff:ff:ff:ff:ff")
print(f" • Layer Ethernet: dst={ether_layer.dst} (broadcast)")
# Layer ARP: richiesta per il range specificato
arp_layer = ARP(pdst=target_range)
print(f" • Layer ARP: pdst={arp_layer.pdst}")
print(f" • Operation: {arp_layer.op} (who-has)")
# Combinazione dei layer
packet = ether_layer / arp_layer
print(f" • Pacchetto totale: {len(bytes(packet))} byte")
# Calcolo numero di host da scansionare
# Per una /24: 256 IP - 2 (network e broadcast) = 254 host
if target_range.endswith('/24'):
expected_hosts = 254
else:
# Calcolo approssimativo per altre subnet
import ipaddress
network = ipaddress.ip_network(target_range, strict=False)
expected_hosts = network.num_addresses - 2
print(f" • Host da scansionare: {expected_hosts}")
estimated_time = expected_hosts * inter + timeout
print(f" • Tempo stimato: {estimated_time:.1f} secondi")
# ===== FASE 2: INVIO E RICEZIONE =====
print(f"\n[FASE 2] Invio pacchetti ARP e ascolto risposte...")
print(f" • Inizio scansione alle {time.strftime('%H:%M:%S')}")
# Timestamp iniziale
start_time = time.time()
# INVIO effettivo con srp
try:
answered, unanswered = srp(
packet,
timeout=timeout,
inter=inter,
verbose=1, # Mostra progresso
iface=None, # Interfaccia default
retry=0, # Nessun retry
multi=False # Una risposta per host
)
except PermissionError:
print("\n[ERRORE] Permessi insufficienti!")
print("Esegui lo script con privilegi elevati:")
print(" • Linux/Mac: sudo python3 script.py")
print(" • Windows: Esegui come amministratore")
return None, None, None
except KeyboardInterrupt:
print("\n\n[INTERROTTO] Scansione interrotta dall'utente")
return None, None, None
except Exception as e:
print(f"\n[ERRORE] Errore durante la scansione: {e}")
return None, None, None
# Timestamp finale
end_time = time.time()
elapsed_time = end_time - start_time
print(f" • Fine scansione alle {time.strftime('%H:%M:%S')}")
print(f" • Tempo effettivo: {elapsed_time:.2f} secondi")
# ===== FASE 3: ANALISI STATISTICHE =====
print(f"\n[FASE 3] Analisi risultati...")
num_answered = len(answered)
num_unanswered = len(unanswered)
total_sent = num_answered + num_unanswered
response_rate = (num_answered / total_sent * 100) if total_sent > 0 else 0
packets_per_second = total_sent / elapsed_time if elapsed_time > 0 else 0
# Statistiche
stats = {
'total_sent': total_sent,
'answered': num_answered,
'unanswered': num_unanswered,
'response_rate': response_rate,
'elapsed_time': elapsed_time,
'packets_per_second': packets_per_second
}
# ===== FASE 4: PRESENTAZIONE RISULTATI =====
print(f"\n{'='*70}")
print("RISULTATI SCANSIONE")
print(f"{'='*70}")
print(f"Pacchetti inviati: {stats['total_sent']}")
print(f"Risposte ricevute: {stats['answered']}")
print(f"Mancate risposte: {stats['unanswered']}")
print(f"Tasso di risposta: {stats['response_rate']:.1f}%")
print(f"Tempo impiegato: {stats['elapsed_time']:.2f}s")
print(f"Pacchetti/secondo: {stats['packets_per_second']:.1f}")
print(f"{'='*70}\n")
if num_answered > 0:
print(f"{'IP ADDRESS':<18} {'MAC ADDRESS':<20} {'VENDOR OUI':<15} {'LATENCY'}")
print(f"{'-'*70}")
for sent, received in answered:
# Estrazione dati
ip_addr = received.psrc
mac_addr = received.hwsrc
vendor_oui = mac_addr[:8] # Primi 3 byte (XX:XX:XX)
# Calcolo latenza (differenza tra invio e ricezione)
latency = (received.time - sent.sent_time) * 1000 # in millisecondi
print(f"{ip_addr:<18} {mac_addr:<20} {vendor_oui:<15} {latency:.2f}ms")
# ===== ANALISI AGGIUNTIVE =====
print(f"\n{'='*70}")
print("ANALISI AGGIUNTIVE")
print(f"{'='*70}")
# Host più veloce a rispondere
fastest = min(answered, key=lambda x: x[1].time - x[0].sent_time)
fastest_ip = fastest[1].psrc
fastest_latency = (fastest[1].time - fastest[0].sent_time) * 1000
print(f"Host più veloce: {fastest_ip} ({fastest_latency:.2f}ms)")
# Host più lento a rispondere
slowest = max(answered, key=lambda x: x[1].time - x[0].sent_time)
slowest_ip = slowest[1].psrc
slowest_latency = (slowest[1].time - slowest[0].sent_time) * 1000
print(f"Host più lento: {slowest_ip} ({slowest_latency:.2f}ms)")
# Latenza media
avg_latency = sum((r[1].time - r[0].sent_time) for r in answered) / len(answered) * 1000
print(f"Latenza media: {avg_latency:.2f}ms")
# Analisi vendor (primi 3 byte del MAC identificano il produttore)
vendors = {}
for _, received in answered:
oui = received.hwsrc[:8]
vendors[oui] = vendors.get(oui, 0) + 1
print(f"\nDistribuzione per vendor:")
for vendor, count in sorted(vendors.items(), key=lambda x: x[1], reverse=True):
percentage = (count / num_answered) * 100
print(f" • {vendor}: {count} dispositivi ({percentage:.1f}%)")
else:
print("⚠ NESSUN HOST HA RISPOSTO")
print("\nPossibili cause:")
print(" • Range IP non corretto")
print(" • Host non presenti in rete")
print(" • Firewall che blocca ARP")
print(" • Interfaccia di rete sbagliata")
print("\nSuggerimenti:")
print(" • Verifica la connessione di rete")
print(" • Controlla che il range IP sia corretto")
print(" • Prova ad aumentare il timeout")
print(f" • Specifica l'interfaccia con iface='eth0'")
print(f"\n{'='*70}\n")
return answered, unanswered, stats
# ===== ESEMPIO DI UTILIZZO =====
if __name__ == "__main__":
# Configura il tuo range IP
target_network = "192.168.1.0/24" # Modifica secondo la tua rete
# Esegui la scansione
results = comprehensive_arp_scan(
target_network,
timeout=3, # 3 secondi di attesa
inter=0.05 # 50ms tra pacchetti (20 pacchetti/secondo)
)
if results[0] is not None:
answered, unanswered, statistics = results
# Puoi fare ulteriori elaborazioni sui risultati
print("Esempi di ulteriori analisi:\n")
# 1. Salvare gli IP attivi in un file
with open('active_hosts.txt', 'w') as f:
for sent, received in answered:
f.write(f"{received.psrc}\n")
print("• IP attivi salvati in 'active_hosts.txt'")
# 2. Controllare se un IP specifico è attivo
target_check = "192.168.1.1"
is_active = any(received.psrc == target_check for _, received in answered)
print(f"• {target_check} è {'ATTIVO' if is_active else 'NON ATTIVO'}")
# 3. Contare dispositivi per subnet
subnets = {}
for _, received in answered:
subnet = '.'.join(received.psrc.split('.')[:3])
subnets[subnet] = subnets.get(subnet, 0) + 1
print(f"• Dispositivi per subnet: {subnets}")
Quando eseguite lo script sopra, otterrete un output simile a questo:
======================================================================
SCANSIONE ARP AVANZATA
======================================================================
Target Range: 192.168.1.0/24
Timeout: 3s
Intervallo tra pacchetti: 0.05s
======================================================================
[FASE 1] Costruzione pacchetto ARP...
• Layer Ethernet: dst=ff:ff:ff:ff:ff:ff (broadcast)
• Layer ARP: pdst=192.168.1.0/24
• Operation: who-has (who-has)
• Pacchetto totale: 42 byte
• Host da scansionare: 254
• Tempo stimato: 15.7 secondi
[FASE 2] Invio pacchetti ARP e ascolto risposte...
• Inizio scansione alle 14:23:45
Begin emission:
Finished sending 254 packets.
.*.*.*.*.*.*.*.*
Received 8 packets, got 8 answers, remaining 246 packets
• Fine scansione alle 14:23:58
• Tempo effettivo: 13.42 secondi
[FASE 3] Analisi risultati...
======================================================================
RISULTATI SCANSIONE
======================================================================
Pacchetti inviati: 254
Risposte ricevute: 8
Mancate risposte: 246
Tasso di risposta: 3.1%
Tempo impiegato: 13.42s
Pacchetti/secondo: 18.9
======================================================================
IP ADDRESS MAC ADDRESS VENDOR OUI LATENCY
----------------------------------------------------------------------
192.168.1.1 00:1a:2b:3c:4d:5e 00:1a:2b 2.34ms
192.168.1.10 aa:bb:cc:dd:ee:ff aa:bb:cc 5.67ms
192.168.1.15 11:22:33:44:55:66 11:22:33 3.21ms
192.168.1.20 ff:ee:dd:cc:bb:aa ff:ee:dd 4.89ms
192.168.1.25 12:34:56:78:9a:bc 12:34:56 6.12ms
192.168.1.50 98:76:54:32:10:fe 98:76:54 2.98ms
192.168.1.100 aa:11:bb:22:cc:33 aa:11:bb 7.45ms
192.168.1.200 de:ad:be:ef:ca:fe de:ad:be 3.76ms
======================================================================
ANALISI AGGIUNTIVE
======================================================================
Host più veloce: 192.168.1.1 (2.34ms)
Host più lento: 192.168.1.100 (7.45ms)
Latenza media: 4.55ms
Distribuzione per vendor:
• aa:bb:cc: 2 dispositivi (25.0%)
• 00:1a:2b: 1 dispositivi (12.5%)
• 11:22:33: 1 dispositivi (12.5%)
• ff:ee:dd: 1 dispositivi (12.5%)
• 12:34:56: 1 dispositivi (12.5%)
• 98:76:54: 1 dispositivi (12.5%)
• de:ad:be: 1 dispositivi (12.5%)
======================================================================
Esempi di ulteriori analisi:
• IP attivi salvati in 'active_hosts.txt'
• 192.168.1.1 è ATTIVO
• Dispositivi per subnet: {'192.168.1': 8}
Tasso di risposta 3.1%: In una /24 ci sono 254 host possibili. Solo 8 hanno risposto, quindi solo l’3.1% degli indirizzi IP è attivamente in uso. Questo è normale in molte reti domestiche o piccole aziende.
Latenza: I tempi di risposta (2-7ms) indicano una rete locale in buona salute. Latenze superiori a 20-30ms potrebbero indicare problemi di congestione o dispositivi lenti.
Vendor OUI: I primi 3 byte del MAC identificano il produttore. Dispositivi con lo stesso OUI probabilmente vengono dallo stesso vendor.
Quando si usa srp in produzione o in script automatizzati, è fondamentale gestire gli errori in modo robusto:
Errore 1: Permission Denied (Errno 13)
try:
answered, unanswered = srp(packet, timeout=2, verbose=0)
except PermissionError:
print("[ERRORE] Privilegi insufficienti")
print("Soluzione:")
print(" Linux/Mac: sudo python3 script.py")
print(" Windows: Esegui come Amministratore")
sys.exit(1)
Errore 2: Interface Not Found
from scapy.all import get_if_list
try:
answered, unanswered = srp(packet, iface="eth999", timeout=2)
except OSError as e:
print(f"[ERRORE] Interfaccia non trovata: {e}")
print("Interfacce disponibili:")
for iface in get_if_list():
print(f" • {iface}")
sys.exit(1)
Errore 3: Keyboard Interrupt (Ctrl+C)
try:
answered, unanswered = srp(packet, timeout=10, verbose=1)
except KeyboardInterrupt:
print("\n[INTERROTTO] Scansione fermata dall'utente")
print("Risultati parziali:")
# Puoi comunque processare quello che hai raccolto finora
Errore 4: Timeout troppo breve (nessuna risposta)
answered, unanswered = srp(packet, timeout=2, verbose=0)
if len(answered) == 0:
print("[WARNING] Nessuna risposta ricevuta")
print("Possibili cause:")
print(" • Timeout troppo breve")
print(" • Range IP sbagliato")
print(" • Interfaccia di rete sbagliata")
print(" • Firewall che blocca")
print("\nSuggerimenti:")
print(" • Aumenta timeout a 5 secondi")
print(" • Verifica con: ping 192.168.1.1")
print(" • Controlla interfaccia con: ip addr show")
1. Sempre gestire eccezioni
import sys
def safe_srp(packet, **kwargs):
"""
Wrapper sicuro per srp con gestione errori
"""
try:
return srp(packet, **kwargs)
except PermissionError:
print("Errore: privilegi insufficienti")
sys.exit(1)
except KeyboardInterrupt:
print("\nInterrotto dall'utente")
sys.exit(0)
except Exception as e:
print(f"Errore imprevisto: {e}")
sys.exit(1)
# Uso
answered, unanswered = safe_srp(packet, timeout=2, verbose=0)
2. Usare verbose=0 in script automatizzati
# Script interattivo - mostra progresso
if __name__ == "__main__":
answered, unanswered = srp(packet, timeout=2, verbose=1)
# Script automatizzato/cron job - silenzioso
else:
answered, unanswered = srp(packet, timeout=2, verbose=0)
3. Implementare rate limiting per reti grandi
# Per una /16 (65534 host), non saturare la rete
answered, unanswered = srp(
packet,
timeout=5,
inter=0.1, # 100ms tra pacchetti = 10 pacchetti/secondo
verbose=0
)
# Tempo stimato: 65534 * 0.1 = 6553 secondi ≈ 109 minuti
# Considerare di dividere in subnet più piccole
4. Logging dei risultati
import logging
import json
from datetime import datetime
logging.basicConfig(level=logging.INFO)
def scan_and_log(target_range):
packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=target_range)
answered, unanswered = srp(packet, timeout=2, verbose=0)
# Prepara dati per logging
scan_result = {
'timestamp': datetime.now().isoformat(),
'target': target_range,
'found': len(answered),
'missing': len(unanswered),
'hosts': [
{'ip': r.psrc, 'mac': r.hwsrc}
for s, r in answered
]
}
# Log in formato JSON
logging.info(json.dumps(scan_result, indent=2))
return answered, unanswered
5. Cache dei risultati per evitare scansioni ripetute
import pickle
import os
from datetime import datetime, timedelta
CACHE_FILE = 'arp_cache.pkl'
CACHE_DURATION = timedelta(minutes=5)
def scan_with_cache(target_range):
# Controlla se esiste cache valida
if os.path.exists(CACHE_FILE):
with open(CACHE_FILE, 'rb') as f:
cached = pickle.load(f)
if datetime.now() - cached['timestamp'] < CACHE_DURATION:
print(f"Uso cache (età: {datetime.now() - cached['timestamp']})")
return cached['answered'], cached['unanswered']
# Scansione nuova
print("Eseguo nuova scansione...")
packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=target_range)
answered, unanswered = srp(packet, timeout=2, verbose=0)
# Salva in cache
with open(CACHE_FILE, 'wb') as f:
pickle.dump({
'timestamp': datetime.now(),
'answered': answered,
'unanswered': unanswered
}, f)
return answered, unanswered
srp (ARP Scan):
sr (IP Scan con ICMP):
from scapy.all import IP, ICMP, sr
# IP Scan alternativo
packet = IP(dst="8.8.8.8")/ICMP()
answered, unanswered = sr(packet, timeout=2)
nmap (tool esterno):
Quando usare cosa:
from scapy.all import Ether, ARP, srp
import time
from datetime import datetime
def continuous_network_monitor(target_range, interval=60):
"""
Monitora continuamente la rete e rileva nuovi dispositivi
"""
known_hosts = set()
scan_count = 0
print(f"Avvio monitoraggio continuo di {target_range}")
print(f"Scansione ogni {interval} secondi")
print("Premi Ctrl+C per fermare\n")
try:
while True:
scan_count += 1
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"[{timestamp}] Scansione #{scan_count}...")
# Esegui scansione
packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=target_range)
answered, unanswered = srp(packet, timeout=2, verbose=0, inter=0.05)
# Estrai IP attivi
current_hosts = set(received.psrc for _, received in answered)
# Rileva nuovi host
new_hosts = current_hosts - known_hosts
if new_hosts:
print(f" ⚠️ NUOVI DISPOSITIVI RILEVATI: {new_hosts}")
for ip in new_hosts:
# Trova il MAC del nuovo host
mac = next(r.hwsrc for s, r in answered if r.psrc == ip)
print(f" • {ip} - {mac}")
# Rileva host scomparsi
disappeared = known_hosts - current_hosts
if disappeared:
print(f" ⚠️ DISPOSITIVI SCOMPARSI: {disappeared}")
# Aggiorna known hosts
known_hosts = current_hosts
print(f" ✓ Host attivi: {len(current_hosts)}")
# Attendi prima della prossima scansione
time.sleep(interval)
except KeyboardInterrupt:
print(f"\n\nMonitoraggio terminato dopo {scan_count} scansioni")
print(f"Ultimo stato: {len(known_hosts)} host attivi")
# Avvio
# continuous_network_monitor("192.168.1.0/24", interval=60)
Problema: srp non cattura risposte su VM/Docker
Soluzione: Configurare la scheda di rete in modalità promiscua
# Verificare modalità promiscua
from scapy.all import conf
print(f"Promiscuous mode: {conf.promisc}")
# Forzare modalità promiscua
conf.promisc = True
# Oppure specificare nel parametro srp
answered, unanswered = srp(packet, promisc=True, timeout=2)
Problema: Su Windows, srp non funziona
Soluzione: Installare WinPcap o Npcap
# Download Npcap da: https://npcap.com/
# Installare con opzione "WinPcap Compatibility Mode"
Verifica installazione:
from scapy.all import show_interfaces
show_interfaces() # Dovrebbe mostrare le interfacce
Problema: srp riceve risposte duplicate
Causa: Switch che fanno flooding o broadcast storms
Soluzione: Usare multi=False (default) e filtrare duplicati
answered, unanswered = srp(packet, timeout=2, multi=False)
# Filtrare manualmente duplicati per IP
seen_ips = set()
unique_answers = []
for sent, received in answered:
if received.psrc not in seen_ips:
seen_ips.add(received.psrc)
unique_answers.append((sent, received))
Problema: Latenza molto alta (>100ms) su LAN
Possibili cause:
Diagnosi:
# Misurare latenza individuale
for sent, received in answered:
latency = (received.time - sent.sent_time) * 1000
if latency > 100:
print(f"⚠️ {received.psrc}: latenza alta {latency:.2f}ms")
from scapy.all import Ether, ARP, srp
import time
def advanced_network_scan(network_range):
"""
Scansione di rete con statistiche dettagliate
"""
# Preparazione
arp = ARP(pdst=network_range)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether/arp
# Statistiche
start_time = time.time()
# Esecuzione scan
answered, unanswered = srp(
packet,
timeout=3,
verbose=0,
inter=0.05 # 50ms tra pacchetti per non saturare la rete
)
end_time = time.time()
# Risultati
print("=" * 60)
print("NETWORK SCAN REPORT")
print("=" * 60)
print(f"Range scansionato: {network_range}")
print(f"Tempo di esecuzione: {end_time - start_time:.2f} secondi")
print(f"Host attivi: {len(answered)}")
print(f"Host non raggiungibili: {len(unanswered)}")
print("\n" + "=" * 60)
print(f"{'IP ADDRESS':<20} {'MAC ADDRESS':<20} {'VENDOR'}")
print("=" * 60)
for sent, received in answered:
# Puoi integrare lookup del vendor dal MAC
print(f"{received.psrc:<20} {received.hwsrc:<20}")
print("=" * 60)
return answered, unanswered
# Utilizzo
results = advanced_network_scan("192.168.1.0/24")
È importante distinguere tra srp() (send and receive) e sendp() (solo send):
from scapy.all import sendp, srp
# sendp() - Solo invio, nessuna ricezione
sendp(packet, iface="eth0")
# Utile per: generazione di traffico, DoS testing (etico), simulazioni
# srp() - Invio E ricezione
answered, unanswered = srp(packet, iface="eth0", timeout=2)
# Utile per: discovery, probing, testing con verifica di risposta
Timeout adeguato: Imposta un timeout ragionevole in base alla dimensione della rete
# Rete piccola (< 50 host)
srp(packet, timeout=1)
# Rete media (50-200 host)
srp(packet, timeout=2)
# Rete grande (> 200 host)
srp(packet, timeout=3)
Rate limiting: Non saturare la rete con troppi pacchetti simultanei
srp(packet, inter=0.1) # 10 pacchetti/secondo
Gestione errori: Sempre gestire eccezioni in ambiente di produzione
try:
answered, unanswered = srp(packet, timeout=2, verbose=0)
except PermissionError:
print("Errore: sono necessari privilegi di amministratore")
except Exception as e:
print(f"Errore durante l'invio: {e}")
Interfaccia specifica: In sistemi con più interfacce, specifica quale usare
srp(packet, iface="eth0", timeout=2)
Mettiamo insieme tutto quello che abbiamo imparato in uno script completo e commentato:
#!/usr/bin/env python3
"""
Script di Network Discovery e Analysis
Utilizza Scapy per scoprire e analizzare host in una rete locale
"""
from scapy.all import Ether, ARP, srp, hexdump
import sys
def discover_network(target_range):
"""
Esegue una scansione ARP della rete e visualizza risultati dettagliati
Args:
target_range (str): Range IP in notazione CIDR (es. "192.168.1.0/24")
Returns:
tuple: (answered, unanswered) - risultati della scansione
"""
print(f"\n{'=' * 70}")
print(f"SCANSIONE RETE: {target_range}")
print(f"{'=' * 70}\n")
# FASE 1: Costruzione del pacchetto
print("[FASE 1] Costruzione pacchetto ARP...")
# Layer 2: Frame Ethernet (broadcast)
ether_layer = Ether(dst="ff:ff:ff:ff:ff:ff")
# Layer ARP: Request per il range specificato
arp_layer = ARP(pdst=target_range)
# Combinazione dei layer
packet = ether_layer / arp_layer
# Visualizzazione struttura pacchetto
print("\nStruttura del pacchetto:")
packet.show()
# Visualizzazione esadecimale
print("\nRappresentazione esadecimale:")
hexdump(packet)
# FASE 2: Invio e ricezione
print(f"\n[FASE 2] Invio pacchetti e ascolto risposte...")
print(f"Timeout: 3 secondi")
print(f"Intervallo tra pacchetti: 0.1 secondi\n")
try:
answered, unanswered = srp(
packet,
timeout=3,
verbose=1,
inter=0.1
)
except PermissionError:
print("\n[ERRORE] Sono richiesti privilegi di amministratore!")
print("Esegui lo script con: sudo python3 script.py")
sys.exit(1)
except Exception as e:
print(f"\n[ERRORE] Si è verificato un errore: {e}")
sys.exit(1)
# FASE 3: Analisi risultati
print(f"\n{'=' * 70}")
print("[FASE 3] RISULTATI SCANSIONE")
print(f"{'=' * 70}\n")
print(f"Host che hanno risposto: {len(answered)}")
print(f"Host non raggiungibili: {len(unanswered)}")
if answered:
print(f"\n{'IP ADDRESS':<20} {'MAC ADDRESS':<20} {'STATUS'}")
print("-" * 60)
for sent, received in answered:
print(f"{received.psrc:<20} {received.hwsrc:<20} ATTIVO")
# Analisi dettagliata del primo host trovato
if received == answered[0][1]:
print(f"\n{'=' * 70}")
print("ANALISI DETTAGLIATA PRIMO HOST")
print(f"{'=' * 70}")
print("\nPacchetto ricevuto:")
received.show()
print("\nDati grezzi (hexdump):")
hexdump(received)
else:
print("\nNessun host ha risposto alla scansione!")
return answered, unanswered
def analyze_single_host(ip_address):
"""
Analizza un singolo host specifico
Args:
ip_address (str): Indirizzo IP dell'host da analizzare
"""
print(f"\n{'=' * 70}")
print(f"ANALISI HOST: {ip_address}")
print(f"{'=' * 70}\n")
# Costruzione pacchetto per singolo host
packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=ip_address)
# Invio con possibilità di risposte multiple
answered, unanswered = srp(
packet,
timeout=2,
verbose=0,
multi=True # Permette di rilevare IP duplicati
)
if not answered:
print(f"L'host {ip_address} non ha risposto")
return
if len(answered) > 1:
print(f"ATTENZIONE: Rilevati {len(answered)} dispositivi con lo stesso IP!")
print("Possibile conflitto di indirizzi IP\n")
for idx, (sent, received) in enumerate(answered, 1):
print(f"Risposta #{idx}:")
print(f" IP: {received.psrc}")
print(f" MAC: {received.hwsrc}")
print(f" Tempo di risposta: {received.time - sent.sent_time:.6f}s")
print()
def main():
"""
Funzione principale
"""
# Configurazione
network_range = "192.168.1.0/24" # Modifica secondo la tua rete
print("""
╔═══════════════════════════════════════════════════════════════╗
║ SCAPY NETWORK DISCOVERY & ANALYSIS TOOL ║
║ ║
║ Questo script dimostra l'uso di: ║
║ - Ether: Costruzione frame Ethernet ║
║ - ARP: Address Resolution Protocol ║
║ - srp: Send and Receive Packets (Layer 2) ║
║ - hexdump: Visualizzazione esadecimale ║
╚═══════════════════════════════════════════════════════════════╝
""")
# Esegui scansione completa
answered, unanswered = discover_network(network_range)
# Se ci sono host attivi, analizza il primo in dettaglio
if answered:
first_host_ip = answered[0][1].psrc
input(f"\nPremi INVIO per analizzare in dettaglio l'host {first_host_ip}...")
analyze_single_host(first_host_ip)
print(f"\n{'=' * 70}")
print("SCANSIONE COMPLETATA")
print(f"{'=' * 70}\n")
if __name__ == "__main__":
main()
Scapy è uno strumento estremamente potente che permette di manipolare il traffico di rete a basso livello. Con questo potere viene la responsabilità di usarlo eticamente e legalmente.
✅ Usi legittimi:
❌ Usi illegali:
Obiettivo: Scoprire tutti i dispositivi nella tua rete locale
# Completa il codice
from scapy.all import Ether, ARP, srp
# TODO: Definisci il range IP della tua rete
target = "___.___.___.___ /___"
# TODO: Costruisci il pacchetto ARP
packet = ___
# TODO: Invia e ricevi
answered, unanswered = ___
# TODO: Stampa i risultati
for ___, ___ in answered:
print(f"IP: ___ - MAC: ___")
Obiettivo: Creare un pacchetto e analizzare la sua struttura esadecimale
from scapy.all import Ether, ARP, hexdump
# TODO: Crea un pacchetto ARP request per 192.168.1.1
packet = ___
# TODO: Visualizza il pacchetto in formato strutturato
___
# TODO: Visualizza il pacchetto in formato esadecimale
___
# TODO: Identifica manualmente nel hexdump:
# - Dove inizia l'indirizzo MAC di destinazione?
# - Dove si trova il campo 'operation' di ARP?
# - Dove è codificato l'indirizzo IP 192.168.1.1?
Obiettivo: Implementare un sistema di rilevamento conflitti IP
from scapy.all import Ether, ARP, srp
def find_duplicate_ips(network_range):
"""
TODO: Implementa una funzione che:
1. Scansiona il network_range
2. Identifica IP usati da più dispositivi
3. Stampa un report dei conflitti trovati
"""
pass
# Test
find_duplicate_ips("192.168.1.0/24")
Obiettivo: Creare uno script che genera statistiche di rete
from scapy.all import Ether, ARP, srp
import time
def network_statistics(network_range):
"""
TODO: Implementa uno script che calcola:
- Tempo medio di risposta degli host
- Percentuale di host attivi vs totali possibili
- Lista vendor dei MAC address (ricerca online)
- Grafico della distribuzione IP
"""
pass
Sintomo: Errore quando si cerca di inviare pacchetti
Soluzione:
# Linux/Mac
sudo python3 script.py
# Windows
# Esegui il terminale come amministratore
Sintomo: Python non trova il modulo Scapy
Soluzione:
pip install scapy
# oppure
pip3 install scapy
Possibili cause:
Soluzione:
from scapy.all import get_if_list, conf
# Verifica interfacce disponibili
print(get_if_list())
# Specifica l'interfaccia corretta
srp(packet, iface="eth0", timeout=3)
Causa: Interfaccia di rete in modalità promiscua non supportata o virtualizata
Soluzione: Verifica le impostazioni della scheda di rete virtuale se stai usando VM
In questa guida abbiamo esplorato in profondità quattro componenti fondamentali di Scapy:
Padroneggiare questi concetti ti permette di:
Scapy è uno strumento che cresce con te: più lo usi, più scopri le sue potenzialità. Inizia con esempi semplici, sperimenta in ambienti sicuri, e gradualmente costruisci script sempre più complessi.
Ricorda: con grande potere viene grande responsabilità. Usa sempre Scapy in modo etico e legale.
from scapy.all import Ether, ARP, hexdump, srp
# Broadcast
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
# Unicast
ether = Ether(dst="aa:bb:cc:dd:ee:ff", src="11:22:33:44:55:66")
# ARP Request
arp = ARP(pdst="192.168.1.1")
# ARP Reply
arp = ARP(op=2, hwsrc="aa:bb:cc:dd:ee:ff", psrc="192.168.1.100")
# Send and Receive (Layer 2)
answered, unanswered = srp(packet, timeout=2, verbose=0)
# Vista strutturata
packet.show()
# Vista esadecimale
hexdump(packet)
for sent, received in answered:
print(f"IP: {received.psrc}, MAC: {received.hwsrc}")
Versione: 1.0
Ultima modifica: Gennaio 2025
Autore: Guida didattica per studenti ITI e Universitari
Licenza: Materiale educativo - Uso libero per scopi didattici